package com.xaicode.ca;

import com.isoftstone.vsm.config.properties.CAProperties;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.Certificate;
import org.bouncycastle.asn1.x509.*;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.crypto.params.AsymmetricKeyParameter;
import org.bouncycastle.crypto.util.PrivateKeyFactory;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder;
import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import javax.security.auth.x500.X500Principal;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.CertificateFactory;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.Objects;

/**
 * 证书授权工具
 *
 * @author Locker xaicode@sina.com
 */
@Slf4j
@Component
public class CertificationAuthority {

    private final static String RSA_PRIVATE_KEY_PREFIX = "-----BEGIN RSA PRIVATE KEY-----";
    private final static String RSA_PRIVATE_KEY_SUFFIX = "-----END RSA PRIVATE KEY-----";
    private final static String CERTIFICATE_REQUEST_PREFIX = "-----BEGIN CERTIFICATE REQUEST-----";
    private final static String CERTIFICATE_REQUEST_SUFFIX = "-----END CERTIFICATE REQUEST-----";
    private final static String CERTIFICATE_PREFIX = "-----BEGIN CERTIFICATE-----";
    private final static String CERTIFICATE_SUFFIX = "-----END CERTIFICATE-----";
    private final static String PUBLIC_KEY_PREFIX = "-----BEGIN PUBLIC KEY-----";
    private final static String PUBLIC_KEY_SUFFIX = "-----END PUBLIC KEY-----";
    private final static String LINE_BREAK = "\n";
    private final static String ENTER_BREAK = "\r";
    private final static byte LINE_BREAK_BYTE = '\n';

    /**
     * 证书类型
     */
    private final static String CERTIFICATE_TYPE = "X.509";

    /**
     * 证书提供者
     */
    private final static String PROVIDER_BC = "BC";

    private final CAProperties caProperties;

    public CertificationAuthority(CAProperties caProperties) {
        this.caProperties = caProperties;
    }

    /**
     * 创建请发请求 csr
     *
     * @param clientDistinguishedName 客户专用名称，用于创建客户签名请求，
     *                                格式要求：CN=customName
     */
    public CsrAndKeyPair genCertificateSigningRequest(String clientDistinguishedName) {

        Objects.requireNonNull(clientDistinguishedName, "- clientDistinguishedName can not be null");

//        if (!clientDistinguishedName.toUpperCase().startsWith("CN=")) {
//            clientDistinguishedName = "CN=" + clientDistinguishedName;
//        }

        /*
            将提供者添加到下一个可用位置。
            如果有安全管理器，则使用“insertProvider”权限目标名称调用SecurityManager.checkSecurityAccess方法，以查看是否可以添加新的提供程序。
            如果拒绝此权限检查，则使用“insertProvider.” + provider.getName() 权限目标名称再次调用checkSecurityAccess。
            如果两个检查都被拒绝，则抛出SecurityException。
         */
        Security.addProvider(new BouncyCastleProvider());

        try {
            // 初始化加密算法和长度
            KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(caProperties.getAlgorithm());
            keyPairGenerator.initialize(caProperties.getKeySize());

            // 获取公钥、私钥
            KeyPair keyPair = keyPairGenerator.generateKeyPair();
            PublicKey publicKey = keyPair.getPublic();
            PrivateKey privateKey = keyPair.getPrivate();

            // 创建客户签名
            X500Principal subject = new X500Principal(clientDistinguishedName);
            // 以私钥加密签发内容，仅允许配对的公钥解密
            ContentSigner contentSigner = new JcaContentSignerBuilder(caProperties.getSignAlgorithm()).build(privateKey);
            // 创建认证请求，包含客户信息和公钥
            PKCS10CertificationRequestBuilder builder =
                    new JcaPKCS10CertificationRequestBuilder(subject, publicKey);
            PKCS10CertificationRequest csr = builder.build(contentSigner);

            // 创建签发请求和公钥信息
            return new CsrAndKeyPair(csr, keyPair);

        } catch (NoSuchAlgorithmException | OperatorCreationException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取根私钥
     *
     * @param privateKey 私钥内容或私钥文件路径
     * @return 私钥
     */
    public PrivateKey readPrivateKey(String privateKey) {
        if (null == privateKey) {
            throw new RuntimeException("- privateKey can not be null");
        }

        Security.addProvider(new BouncyCastleProvider());

        try {

            // 读取根私钥
            String rootPrivateKeyPEM;
            if (privateKey.startsWith(RSA_PRIVATE_KEY_PREFIX)) {
                rootPrivateKeyPEM = privateKey
                        .replace(RSA_PRIVATE_KEY_PREFIX, "")
                        .replace(RSA_PRIVATE_KEY_SUFFIX, "");
            } else {

                // 开发环境可用
//                File rootPrivateKeyFile = ResourceUtils.getFile(privateKey);
//                FileInputStream fis = new FileInputStream(rootPrivateKeyFile);
//                DataInputStream dis = new DataInputStream(fis);
//                byte[] keyBytes = new byte[(int) rootPrivateKeyFile.length()];

                // springboot jar 环境下，File() 无法获取 resource 下的文件
                Resource resource = new ClassPathResource(privateKey);
                InputStream fis = resource.getInputStream();
                DataInputStream dis = new DataInputStream(fis);
                byte[] keyBytes = new byte[Integer.parseInt(String.valueOf(resource.contentLength()))];

                dis.readFully(keyBytes);

                rootPrivateKeyPEM = new String(keyBytes)
                        .replace(RSA_PRIVATE_KEY_PREFIX, "")
                        .replace(RSA_PRIVATE_KEY_SUFFIX, "");

                dis.close();
                fis.close();
            }

            // 加入 \r
            if (privateKey.startsWith(RSA_PRIVATE_KEY_PREFIX)) {
//                rootPrivateKeyPEM = rootPrivateKeyPEM.replace("\n", "\r\n");
            }

            // base64 解密
            byte[] decodeKey = Base64.getMimeDecoder().decode(rootPrivateKeyPEM);

            // 生成私有密钥信息，该信息包括公开密钥算法的私有密钥以及可选的属性集等
            PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decodeKey);
            KeyFactory keyFactory = KeyFactory.getInstance(caProperties.getAlgorithm());

            return keyFactory.generatePrivate(spec);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 签发证书，指定生效时间和有效期
     *
     * @param csr                 认证请求
     * @param caPrivateKey        ca私钥
     * @param pair                秘钥对
     * @param caDistinguishedName 证书颁发者专用名称，用于创建客户签名请求，
     *                            格式要求：OU=organizationUnit, O=organization, L=locality, ST=state, C=commonName
     * @param serialNumber        证书序号，禁止设置 0L
     * @param startTime           证书签发时间戳
     * @param endTime             证书有效期结束时间戳
     * @return 证书信息
     */
    public CertificateAndKey signCertificationRequest(PKCS10CertificationRequest csr,
                                                      PrivateKey caPrivateKey, KeyPair pair,
                                                      String caDistinguishedName, long serialNumber,
                                                      Long startTime, Long endTime) {

        Security.addProvider(new BouncyCastleProvider());

        try {

            // 创建签名算法
            AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find(caProperties.getSignAlgorithm());
            // 创建摘要算法
            AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);
            // 创建非对称密钥参数
            AsymmetricKeyParameter asymKeyParam = PrivateKeyFactory.createKey(caPrivateKey.getEncoded());

            // 创建签发对象公钥信息
            SubjectPublicKeyInfo keyInfo = SubjectPublicKeyInfo.getInstance(pair.getPublic().getEncoded());

            // 创建 X509 证书描述
            X509v3CertificateBuilder myCertificateGenerator = new X509v3CertificateBuilder(
                    // 创建证书颁发者对象
                    new X500Name(caDistinguishedName),
                    // 证书序号
                    new BigInteger(String.valueOf(serialNumber)),
                    // 开始时间
                    new Date(startTime),
                    // 结束时间
                    new Date(endTime),
                    // 授权对象
                    csr.getSubject(),
                    // 授权对象公钥信息
                    keyInfo
            );

            // 从关键 basicConstraints 扩展 (oid = 2.5.29.19) 中获取证书的限制路径长度
            myCertificateGenerator.addExtension(
                    // OID 2.5.29.19
                    Extension.basicConstraints,
                    true,
                    new BasicConstraints(true)
            );

            // 创建内容签发对象
            ContentSigner sigGen = new BcRSAContentSignerBuilder(sigAlgId, digAlgId).build(asymKeyParam);
            X509CertificateHolder holder = myCertificateGenerator.build(sigGen);
            Certificate x509CertificateStructure = holder.toASN1Structure();

            // 证书信息 base64 编码
            CertificateAndKey certificateAndKey = new CertificateAndKey();
            String base64Cert = Base64.getMimeEncoder(64, new byte[]{LINE_BREAK_BYTE})
                    .encodeToString(x509CertificateStructure.getEncoded());
            // 加入固定开头、结尾标识
            base64Cert = CERTIFICATE_PREFIX + LINE_BREAK + base64Cert + LINE_BREAK + CERTIFICATE_SUFFIX + LINE_BREAK;
            certificateAndKey.setCertificate(base64Cert);

            // 私钥 base64 编码
            Base64Key base64Key = new Base64Key();
            String base64PrivateKey = Base64.getMimeEncoder(64, new byte[]{LINE_BREAK_BYTE})
                    .encodeToString(pair.getPrivate().getEncoded());
            base64PrivateKey = RSA_PRIVATE_KEY_PREFIX + LINE_BREAK + base64PrivateKey + LINE_BREAK + RSA_PRIVATE_KEY_SUFFIX + LINE_BREAK;
            base64Key.setBase64PrivateKey(base64PrivateKey);

            // 公钥 base64 编码
            String base64PublicKey = Base64.getMimeEncoder(64, new byte[]{LINE_BREAK_BYTE})
                    .encodeToString(pair.getPublic().getEncoded());
            base64PublicKey = PUBLIC_KEY_PREFIX + LINE_BREAK + base64PublicKey + LINE_BREAK + PUBLIC_KEY_SUFFIX + LINE_BREAK;
            base64Key.setBase64PublicKey(base64PublicKey);

            certificateAndKey.setBase64Key(base64Key);

            /*
                返回指定证书类型的证书工厂对象。
                返回一个封装了指定提供者的CertificateFactorySpi实现的新CertificateFactory对象。
                指定的提供者必须在安全提供者列表中注册。
                请注意，可以通过Security.getProviders()方法检索注册提供者的列表。
                此处可获取签发的证书的公钥等信息，例：certificate.getPublicKey()，后续可做 md5 或 sha256 等摘要。
             */
            CertificateFactory cf = CertificateFactory.getInstance(CERTIFICATE_TYPE, PROVIDER_BC);
            ByteArrayInputStream x509CertificateStructureInputStream = new ByteArrayInputStream(x509CertificateStructure.getEncoded());
            java.security.cert.Certificate theCert = cf.generateCertificate(x509CertificateStructureInputStream);
            x509CertificateStructureInputStream.close();

            log.debug(theCert.toString());

            return certificateAndKey;

        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 签发证书，证书立即生效，有效期使用默认配置
     *
     * @param csr                 认证请求
     * @param caPrivateKey        ca私钥
     * @param pair                秘钥对
     * @param caDistinguishedName 证书颁发者专用名称，用于创建客户签名请求，
     *                            格式要求：OU=organizationUnit, O=organization, L=locality, ST=state, C=commonName
     * @param serialNumber        证书序号，禁止设置 0L
     * @return 证书信息
     */
    public CertificateAndKey signCertificationRequest(PKCS10CertificationRequest csr,
                                                      PrivateKey caPrivateKey, KeyPair pair,
                                                      String caDistinguishedName, long serialNumber) {
        return signCertificationRequest(csr, caPrivateKey, pair, caDistinguishedName, serialNumber,
                System.currentTimeMillis(),
                System.currentTimeMillis() + caProperties.getSignValidity() * 86400000L);
    }

    /**
     * 创建证书，指定生效时间和有效期
     *
     * @param privateKey              签发证书的私钥，即要生成的证书的父级证书的私钥，
     *                                可为私钥内容或私钥地址
     * @param clientDistinguishedName 客户专用名称，用于创建客户签名请求,
     *                                格式要求：OU=organizationUnit, O=organization, L=locality, ST=state, C=commonName
     *                                为空时，读取配置中颁发者信息，即为自己签发证书，用于签发根证书。
     * @param caDistinguishedName     证书颁发者专用名称，用于创建客户签名请求，
     *                                格式要求：OU=organizationUnit, O=organization, L=locality, ST=state, C=commonName
     *                                <br> 参考{@link CAProperties}
     * @param serialNumber            证书序号，禁止设置 0L
     * @param startTime               证书签发时间戳
     * @param endTime                 证书有效期结束时间戳
     * @return 证书信息
     */
    public CertificateAndKey create(String privateKey, String clientDistinguishedName,
                                    String caDistinguishedName, long serialNumber,
                                    Long startTime, Long endTime) {

        CsrAndKeyPair csrAndKeyPair = genCertificateSigningRequest(clientDistinguishedName);
        PrivateKey caPrivateKey = readPrivateKey(privateKey);
        return signCertificationRequest(csrAndKeyPair.getCsr(), caPrivateKey, csrAndKeyPair.getPair(),
                caDistinguishedName, serialNumber, startTime, endTime);
    }

}
